package org.bhf.security.authorization;

import com.google.inject.Inject;
import org.bhf.providers.security.authorization.PolicyProvider;
import org.bhf.security.common.ContextSubject;
import org.bhf.security.common.PermissionPrincipal;
import org.bhf.security.common.Role;
import org.bhf.security.common.UserID;

import javax.security.auth.Subject;
import java.lang.ref.SoftReference;
import java.security.Permission;
import java.security.PermissionCollection;
import java.security.Principal;
import java.util.Date;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;

/**
 * A security manager to policy resource access.
 */
public class RoleBasedSecurityManager
{
    private static final Principal  ALL_ROLES       = new Role( "*" );
    private static final Principal  ALL_USER_IDS    = new UserID( "*" );

    @Inject
          private PolicyProvider policyProvider;
    final private Map<UserID,SoftReference<SubjectPermissions>> policyCache = new HashMap<UserID,SoftReference<SubjectPermissions>>();

    //
    // Check Permissions
    //

    /**
     * Check whether the given user, presented by the current contextual subject, has access to the requested
     * resource, represented by <code>permission</code>, within the security context of the given
     * contextual project.
     *
     * @param permission the <code>Permission</code> to check.
     *      Must not be <code>null</code>.
     *
     * @throws java.lang.SecurityException if the permission is denied
     * @throws java.lang.IllegalArgumentException if <code>project == null</code>, or <code>subject == null</code>,
     *      or <code>permission == null</code>.
     */
    public void                 check( final Permission permission )
    {
        check( ContextSubject.getContextSubject(), permission );
    }

    /**
     * Check whether the given user, presented by <code>subject</code>, has access to the requested
     * resource, represented by <code>permission</code>, within the security context of the given
     * project.
     *
     * @param subject <code>Subject</code> representing the 'user' requesting access to the resource
     *      represented by <code>permission</code>. Must not be <code>null</code>.
     * @param permission the <code>Permission</code> to check.
     *      Must not be <code>null</code>.
     *
     * @throws java.lang.SecurityException if the permission is denied
     * @throws java.lang.IllegalArgumentException if <code>project == null</code>, or <code>subject == null</code>,
     *      or <code>permission == null</code>.
     */
    public void                 check( final Subject subject, final Permission permission )
    {
        if( permission == null )
            throw new IllegalArgumentException( "permission == null" );
        if( subject == null )
            throw new IllegalArgumentException( "subject == null" );

        if( subject == ContextSubject.SUPER_USER )
            return;

        final UserID                userID      = UserID.getUserID( subject );
        final SubjectPermissions    permissions = getPermissions( permission, subject, userID );

        // Implied by a dynamic subject permission?
        if( permissions.permissions.implies( permission ) )
            return;

        // Implied by a principal assigned permission?
        for( final PrincipalPermissions principalPermission : permissions.principalPermissions )
            if( principalPermission.permissions.implies( permission ) )
                return;

        // No go
        throw new SecurityException( permission.toString() );
    }

    //
    // Permission collections
    //

    /**
     * Return relevant permissions for the given user, presented by <code>subject</code>,
     * within the security context of the given project.
     *
     * @param subject <code>Subject</code> representing the 'user' requesting access to the resource
     *      represented by <code>permission</code>. Must not be <code>null</code>.
     * @param permission the <code>Permission</code> that determines the class of permission to return.
     *      Must not be <code>null</code>.
     *
     * @return A <code>PermissionCollection</code> The collection of relevant permission. Will not be
     *      <code>null</code> or contain <code>null</code> elements. This value must be treated as
     *      read only.
     * @throws java.lang.IllegalArgumentException if <code>project == null</code>, or <code>subject == null</code>,
     *      or <code>permission == null</code>.
     */
    public SubjectPermissions    getPermissions( final Subject subject, final Permission permission )
    {
        if( permission == null )
            throw new IllegalArgumentException( "permission == null" );
        if( subject == null )
            throw new IllegalArgumentException( "subject == null" );

        return getPermissions( permission, subject, UserID.getUserID( subject ) );
    }

    //
    // Cache management
    //

    /**
     * Notify that the security policy changed. Any caches that depends upon the security policy
     * will be considered stale.
     */
    public void                 notifiedOfPolicyChange()
    {
        synchronized( policyCache )
        {
            policyCache.clear();
        }
    }

    //
    // Private
    //

    private SubjectPermissions  getPermissions( final Permission permission, Subject subject, final UserID userID )
    {
        if( subject == null )
            return new SubjectPermissions( new Permissions(), new PrincipalPermissions[0] );

        SubjectPermissions permissions;
        synchronized( policyCache )
        {
            final SoftReference<SubjectPermissions> ref = policyCache.get( userID );
            permissions = ref != null ? ref.get() : null;
        }

        // Initialize permissions and cache if needed
        if( permissions == null || permissions.isStale() )
        {
            // Initialize empty
            {
                permissions = new SubjectPermissions(
                    new Permissions(),
                    new PrincipalPermissions[ subject.getPrincipals().size() + 2 ]
                );
                int j = 0;
                for( Iterator i = subject.getPrincipals().iterator(); i.hasNext(); j++ )
                    permissions.principalPermissions[ j ] = new PrincipalPermissions( (Principal)i.next(), new Permissions() );
                permissions.principalPermissions[ j++ ] = new PrincipalPermissions( ALL_ROLES, new Permissions() );
                permissions.principalPermissions[ j   ] = new PrincipalPermissions( ALL_USER_IDS, new Permissions() );
            }

            // From subject
            {
                final PermissionPrincipal[] subjectGrants = PermissionPrincipal.getPermissionPrincipal( subject );
                for( final PermissionPrincipal subjectGrant : subjectGrants )
                    permissions.permissions.add( subjectGrant.getPermission() );
            }

            // Cache
            synchronized( policyCache )
            {
                policyCache.put( userID, new SoftReference<SubjectPermissions>( permissions ) );
            }
        }

        // Load from policy if needed
        synchronized( policyCache )
        {
            if( !permissions.loadedPermissionClasses.contains( permission.getClass() ) )
            {
                // Load for each principal (role or userID)
                for( final PrincipalPermissions p : permissions.principalPermissions )
                {
                    loadForPrincipal( policyProvider, permission, p.principal, p.permissions );
                }

                // Mark as loaded
                permissions.loadedPermissionClasses.add( permission.getClass() );
            }
        }

        return permissions;
    }

    private static void     loadForPrincipal(
        final PolicyProvider        policyProvider,
        final Permission            permission,
        final Principal             principal,
        final PermissionCollection  permissions
    )
    {
        final PermissionCollection fromPolicy = policyProvider.getPermissions( permission, principal );

        for( Enumeration e = fromPolicy.elements(); e.hasMoreElements(); )
            permissions.add( (Permission)e.nextElement() );
    }

    //
    // Inner classes
    //

    /**
     * Container for all <code>Permission</code>s assigned to a particular <code>Subject</code>. Must be
     * treated as read only.
     */
    public static final class SubjectPermissions
    {
        /** <code>Permission</code>s assigned directly to the <code>Subject</code>. Will not be <code>null</code>,
         * but may be empty.
         */
        public final Permissions            permissions;

        /** <code>Permission</code>s assigned tp each <code>Principal</code> (role, user ID, ...).
         * Will not be <code>null</code>, but may be empty.
         */
        public final PrincipalPermissions[] principalPermissions;

        /**
         * Timestamp for this cache entry. Nominally, entries older than five minutes are considered stale
         */
        public final Date created;

        /** Class of permissions that have already been loaded */
        final Set<Class> loadedPermissionClasses = new HashSet<Class>( 10 );

        public SubjectPermissions( final Permissions permissions, final PrincipalPermissions[] principalPermissions )
        {
            this.permissions            = permissions;
            this.principalPermissions   = principalPermissions;
            this.created                = new Date();
        }

        boolean isStale()
        {
            return created.getTime() + 5L * 60L * 1000L < System.currentTimeMillis();
        }
    }

    /**
     * @exclude
     */
    public static final class PrincipalPermissions
    {
        /**
         * <code>Principal</code> (role, user ID, ...) to which the <code>Permission</code>s are assigned.
         */
        public final Principal principal;

        /** <code>Permission</code>s assigned to the <code>Principal</code>. Will not be <code>null</code>,
         * but may be empty.
         */
        public final Permissions    permissions;

        public PrincipalPermissions( final Principal principal, final Permissions permissions )
        {
            this.principal      = principal;
            this.permissions    = permissions;
        }
    }
}


